﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Gumini.JulaDB.Provider;
using Remotion.Linq;

namespace Gumini.JulaDB.Linq
{
    /// <summary>
    /// Called by re-linq when a query is to be executed.
    /// </summary>
    public class JulaQueryExecutor : IQueryExecutor
    {
        private readonly JulaDbConnection _connection;

        public JulaQueryExecutor(JulaDbConnection connection)
        {
            _connection = connection;
        }

        /// <summary>
        /// Executes a query with a scalar result, i.e. a query that ends with a result operator such as Count, Sum, or Average.
        /// </summary>
        public T ExecuteScalar<T>(QueryModel queryModel)
        {
            return ExecuteCollection<T>(queryModel).Single();
        }

        /// <summary>
        /// Executes a query with a single result object, i.e. a query that ends with a result operator such as First, Last, Single, Min, or Max.
        /// </summary>
        public T ExecuteSingle<T>(QueryModel queryModel, bool returnDefaultWhenEmpty)
        {
            return returnDefaultWhenEmpty ?
                ExecuteCollection<T>(queryModel).SingleOrDefault() :
                ExecuteCollection<T>(queryModel).Single();
        }

        /// <summary>
        /// Executes a query with a collection result.
        /// </summary>
        public IEnumerable<T> ExecuteCollection<T>(QueryModel queryModel)
        {
            var query = JulaQueryModelVisitor.GenerateQuery(queryModel);
            var adapter = new JulaDbDataAdapter(query, _connection);
            var dataSet = new DataSet();
            adapter.Fill(dataSet, "Results");
            var table = dataSet.Tables[0];

            var results = new List<T>();
            for (int i = 0; i < table.Rows.Count; i++)
            {
                var row = table.Rows[i];
                results.Add(CreateObjectFromDataRow<T>(row));
            }
            return results;
        }

        private T CreateObjectFromDataRow<T>(DataRow row)
        {
            Debug.WriteLine(Utils.ColumnsToString(row.Table));
            Debug.WriteLine(Utils.DataRowToString(row));

            var targetType = typeof(T);

            // if the target type is nullable we extract the underlying type
            if (Utils.IsNullableType(targetType))
                targetType = Nullable.GetUnderlyingType(targetType);

            if (Utils.IsNumericType(targetType))
            {
                if (row.Table.Columns.Count != 1)
                    throw new InvalidOperationException("Expected a single result.");
                
                if (row[0] == DBNull.Value)
                    return default(T);

                return (T)Convert.ChangeType(row[0], targetType);
            }
            else if (Utils.IsAnonymousType(targetType))
            {
                var parameters = new List<object>();
                int i = 0;
                foreach (var info in targetType.GetProperties())
                {
                    var propertyType = info.PropertyType;
                    if (Utils.IsNullableType(propertyType))
                        propertyType = Nullable.GetUnderlyingType(propertyType);

                    var rowValue = row[i++];
                    if (rowValue != DBNull.Value)
                        parameters.Add(Convert.ChangeType(rowValue, propertyType));
                    else
                        parameters.Add(null);
                }
                T result = (T)Activator.CreateInstance(targetType, parameters.ToArray());
                return result;
            }
            else // a non-anonymous object with properties
            {
                T result = Activator.CreateInstance<T>();
                int i = 0;
                foreach (var info in targetType.GetProperties().Where(p => p.CanWrite))
                {
                    var rowValue = row[i++];
                    if (rowValue == DBNull.Value)
                        rowValue = null;
                    // value must be converted first, e.g. when source is decimal and target is integer
                    rowValue = Utils.ConvertTo(info.PropertyType, rowValue);
                    info.SetValue(result, rowValue, null);
                }
                return result;
            }
        }
    }
}
